package com.dbms.storage;

import com.dbms.transaction.LogManager; // 引入日志管理器，用于支持预写日志（WAL）协议

import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

/**
 * BufferPoolManager (缓冲池管理器)
 * 核心功能：管理内存中的数据库页面缓冲池，负责页面的加载、缓存、替换和刷盘，
 * 减少磁盘I/O次数以提高数据库性能。
 * 【已升级】集成了LogManager以遵守WAL（Write-Ahead Logging，预写日志）协议，
 * 确保数据修改的持久性和一致性（日志先于数据刷盘）。
 */
public class BufferPoolManager {
    /** 缓冲池最大容量（可同时缓存的页面数量） */
    private final int poolSize;
    /** 存储缓冲池中的页面数组（每个元素代表一个内存帧Frame） */
    private final Page[] pages;
    /** 磁盘管理器，负责与物理磁盘交互（读取/写入页面） */
    private final DiskManager diskManager;
    /** 日志管理器，负责日志记录和刷盘，支持WAL协议（新增） */
    private final LogManager logManager;

    /** 页表：映射页面ID（pageId）到缓冲池中的帧ID（frameId），快速定位页面是否在缓冲池 */
    private final Map<Integer, Integer> pageTable;
    /** 空闲帧列表：存储当前未使用的帧ID，用于快速分配新帧 */
    private final Deque<Integer> freeList;
    /** LRU替换器：维护帧的访问顺序，用于缓冲池满时选择替换的页面（最近最少使用策略） */
    private final List<Integer> lruReplacer;

    // --- 【新增】命中率统计变量 ---
    /** 总访问次数：记录fetchPage被调用的总次数，用于计算命中率 */
    private long numAccesses = 0;
    /** 命中次数：记录页面已在缓冲池中的访问次数 */
    private long numHits = 0;

    // --- 修改构造函数，集成日志管理器 ---
    /**
     * 构造缓冲池管理器，初始化核心组件
     * @param poolSize 缓冲池容量（最大页面数）
     * @param diskManager 磁盘管理器实例
     * @param logManager 日志管理器实例（用于WAL协议）
     */
    public BufferPoolManager(int poolSize, DiskManager diskManager, LogManager logManager) {
        this.poolSize = poolSize;
        this.pages = new Page[poolSize]; // 初始化页面数组（帧）
        this.diskManager = diskManager;
        this.logManager = logManager; // 初始化日志管理器

        this.pageTable = new HashMap<>(); // 初始化页表
        this.freeList = new ArrayDeque<>(poolSize); // 初始化空闲帧列表
        this.lruReplacer = new LinkedList<>(); // 初始化LRU替换器

        // 初始化所有帧为空闲状态（页面ID为-1表示未使用）
        for (int i = 0; i < poolSize; i++) {
            this.pages[i] = new Page(-1); // 新建空白页面（页ID为-1）
            this.freeList.add(i); // 将帧ID加入空闲列表
        }
    }

    // --- 【核心修正】创建向后兼容的构造函数 ---
    /**
     * @deprecated 旧版本构造函数，不支持预写日志（WAL），仅用于向后兼容
     * 建议使用带LogManager参数的构造函数以保证数据一致性
     */
    public BufferPoolManager(int poolSize, DiskManager diskManager) {
        this(poolSize, diskManager, null); // 调用主构造函数，日志管理器传null
    }

    /**
     * 将指定脏页刷回磁盘，严格遵守WAL协议（日志先刷盘，再刷数据）
     * @param pageId 要刷回磁盘的页面ID
     * @throws IOException 磁盘I/O操作可能抛出的异常
     */
    private synchronized void flushPage(int pageId) throws IOException {
        // 页面不在缓冲池，直接返回
        if (!pageTable.containsKey(pageId)) return;

        // 从页表获取页面所在的帧ID，进而获取页面对象
        int frameId = pageTable.get(pageId);
        Page page = pages[frameId];

        // 仅当页面是脏页（被修改过）时才需要刷盘
        if (page.isDirty()) {
            // --- 【核心改造】执行WAL协议 ---
            // 关键：在写入数据页到磁盘前，强制将所有日志记录刷盘，确保故障恢复时可重放
            if (logManager != null) {
                logManager.flushLog();
            }

            // 将页面数据写入磁盘
            diskManager.writePage(page.getPageId(), page.getData());
            // 刷盘后，页面不再是脏页
            page.setDirty(false);
        }
    }

    /**
     * 将缓冲池中所有脏页刷回磁盘，用于事务提交或关闭数据库时确保数据持久化
     * @throws IOException 磁盘I/O操作可能抛出的异常
     */
    public synchronized void flushAllPages() throws IOException {
        // 先一次性刷写所有日志（WAL核心：日志优先）
        if (logManager != null) {
            logManager.flushLog();
        }

        // 遍历所有缓冲池中的页面，刷写脏页
        for (Integer pageId : pageTable.keySet()) {
            Page page = pages[pageTable.get(pageId)];
            if (page.isDirty()) {
                diskManager.writePage(page.getPageId(), page.getData());
                page.setDirty(false); // 标记为非脏页
            }
        }
    }

    /**
     * 获取指定页面：优先从缓冲池获取，未命中则从磁盘加载并缓存
     * @param pageId 要获取的页面ID
     * @return 页面对象（Page）；若缓冲池满且所有页面被钉死，返回null
     * @throws IOException 磁盘读取可能抛出的异常
     */
    public synchronized Page fetchPage(int pageId) throws IOException {
        numAccesses++; // 【新增】统计：总访问次数+1

        // 情况1：页面已在缓冲池（命中）
        if (pageTable.containsKey(pageId)) {
            numHits++; // 【新增】统计：命中次数+1
            int frameId = pageTable.get(pageId);
            Page page = pages[frameId];

            // 增加页面的钉住计数（pin count）：表示页面正在被使用，不能被替换
            page.incrementPinCount();

            // 更新LRU队列：将当前页面移到队尾（标记为最近使用）
            lruReplacer.remove((Integer) frameId); // 先从队列移除
            lruReplacer.add(frameId); // 再添加到队尾

            return page;
        }

        // 情况2：页面不在缓冲池（未命中），需从磁盘加载
        // 查找可用帧（空闲帧或可替换的帧）
        Integer frameId = findAvailableFrame();
        if (frameId == null) {
            System.err.println("缓冲池已满且所有页面都被钉死，无法获取新页面！");
            return null;
        }

        // 获取目标帧对应的页面对象
        Page page = pages[frameId];

        // 若该帧之前缓存了其他页面，需从页表中移除旧页面的映射
        if (page.getPageId() != -1) {
            pageTable.remove(page.getPageId());
        }

        // 从磁盘读取目标页面数据到帧中
        diskManager.readPage(pageId, page.getData());
        page.reset(); // 重置页面状态（pin计数、脏页标记等）
        page.setPageId(pageId); // 设置新页面ID
        page.incrementPinCount(); // 钉住页面（当前操作正在使用）

        // 更新页表和LRU队列
        pageTable.put(pageId, frameId);
        lruReplacer.add(frameId); // 新加载的页面加入LRU队尾（最近使用）

        return page;
    }

    /**
     * 查找可用帧：优先使用空闲帧；无空闲帧则通过LRU策略替换未被钉住的页面
     * @return 可用帧ID；若缓冲池满且所有页面被钉住，返回null
     * @throws IOException 刷写脏页到磁盘时可能抛出的异常
     */
    private Integer findAvailableFrame() throws IOException {
        // 1. 优先使用空闲帧
        if (!freeList.isEmpty()) {
            return freeList.poll(); // 从空闲列表头部取一个帧
        }

        // 2. 无空闲帧，使用LRU策略查找可替换的帧（未被钉住的页面）
        if (!lruReplacer.isEmpty()) {
            for (int i = 0; i < lruReplacer.size(); i++) {
                int frameId = lruReplacer.get(i);
                Page victim = pages[frameId];

                // 找到pin计数为0的页面（未被使用，可以替换）
                if (victim.getPinCount() == 0) {
                    lruReplacer.remove(i); // 从LRU队列移除该帧

                    // 若被替换的页面是脏页，先刷回磁盘
                    if (victim.isDirty()) {
                        flushPage(victim.getPageId());
                    }

                    return frameId; // 返回可使用的帧ID
                }
            }
        }

        // 3. 无可用帧（所有页面都被钉住）
        return null;
    }

    /**
     * 解除页面的钉住状态（减少pin计数），并标记页面是否为脏页
     * @param pageId 要解除钉住的页面ID
     * @param isDirty 页面是否被修改（true表示脏页）
     * @return 操作是否成功（页面不在缓冲池或pin计数已为0时返回false）
     */
    public synchronized boolean unpinPage(int pageId, boolean isDirty) {
        // 页面不在缓冲池，返回失败
        if (!pageTable.containsKey(pageId)) {
            return false;
        }

        // 获取页面所在帧和页面对象
        int frameId = pageTable.get(pageId);
        Page page = pages[frameId];

        // pin计数已为0，无法再减少，返回失败
        if (page.getPinCount() <= 0) {
            return false;
        }

        // 减少pin计数（页面使用结束）
        page.decrementPinCount();

        // 若需要标记为脏页，更新页面状态
        if (isDirty) {
            page.setDirty(true);
        }

        // 注意：unpin时不直接操作LRU队列，LRU状态仅在fetchPage时更新

        return true;
    }

    /**
     * 创建新页面：分配新的页面ID，在缓冲池中创建空白页面
     * @return 新创建的页面；若缓冲池满且无可用帧，返回null
     * @throws IOException 磁盘分配新页面或刷写旧页面时可能抛出的异常
     */
    public synchronized Page newPage() throws IOException {
        // 查找可用帧
        Integer frameId = findAvailableFrame();
        if (frameId == null) {
            return null; // 无可用帧，返回null
        }

        Page page = pages[frameId];

        // 若该帧之前缓存了其他页面，从页表和LRU队列中移除
        if (page.getPageId() != -1) {
            pageTable.remove(page.getPageId());
            lruReplacer.remove((Integer) frameId);
        }

        // 从磁盘管理器分配新的页面ID
        int newPageId = diskManager.allocatePage();

        // 初始化新页面：设置ID、标记为脏页（新页面需写入磁盘）、清空数据
        page.setPageId(newPageId);
        page.setDirty(true);
        Arrays.fill(page.getData(), (byte) 0); // 空白页面初始化为0
        page.incrementPinCount(); // 钉住页面（当前操作正在使用）

        // 更新页表映射
        pageTable.put(newPageId, frameId);

        return page;
    }

    /**
     * 获取磁盘中的总页面数（通过磁盘管理器）
     * @return 磁盘中的页面总数
     */
    public int getNumPages() {
        return diskManager.getNumPages();
    }

    // --- 【新增】用于监控和测试的统计方法 ---

    /**
     * 获取缓冲池命中率统计信息
     * @return 包含总访问次数、命中次数和命中率的格式化字符串
     */
    public String getStats() {
        if (numAccesses == 0) {
            return "Buffer Pool Stats: 0 accesses, 0 hits, Hit Rate: N/A";
        }
        double hitRate = (double) numHits / numAccesses * 100.0;
        return String.format("Buffer Pool Stats: %d accesses, %d hits, Hit Rate: %.2f%%", numAccesses, numHits, hitRate);
    }

    /**
     * 重置命中率统计数据（用于重新开始统计）
     */
    public void resetStats() {
        this.numAccesses = 0;
        this.numHits = 0;
    }

    /**
     * 获取当前LRU替换器中的页面ID列表（按LRU顺序，头部为最可能被替换的页面）
     * @return LRU顺序的页面ID列表
     */
    public List<Integer> getLruList() {
        return lruReplacer.stream()
                .map(frameId -> pages[frameId].getPageId()) // 帧ID转换为页面ID
                .collect(Collectors.toList());
    }
}